package org.raymond.iworks.study.redis.redisson;

/**
 * @author raymond
 * @version V1.0
 * @Description: TODO
 */

import lombok.extern.slf4j.Slf4j;
import org.redisson.Redisson;
import org.redisson.api.RFencedLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;

/**
 * redis提供的分布式锁:
 * 1, 安全性: 保证互斥,任意时间只有一个客户端持有锁.
 * 2, 无死锁: 最终总是能持有锁,哪怕持有锁的客户端被分区或者崩溃.
 * 3, 容错性: 只要大多数redis节点可用,锁就是可用的,可以正常持有和释放.
 *
 * 通常基于redis的单实例的分布式锁:带过期时间的set, 可能存在问题
 * 1, 客户端A在Master上获取锁
 * 2, Master将key同步到Replica之前,Master崩溃了
 * 3, Replica提升为Master
 * 4, 客户端B在新的Master上获取了同样的锁
 * result: 违反了互斥性
 * 某些场景可能允许使用这种复制的方案来做处理.但大多数情况下,需要使用redis推荐的RedLock或基于这种方案的锁
 *
 * 单实例(包括主副复制)的正确实现方式:
 *  set resource_name random_value nx px 3000
 *  random_value是随机的,避免删除其他客户端持有的锁:
 *  1,客户端A获取锁
 *  2,客户端A执行业务超过了超时时间
 *  3,客户端B获取锁
 *  4,客户端A执行完毕,删除Key(此时就会删掉客户端B持有的锁)
 *
 *  因此,使用随机值是为了以安全的方式释放锁,用一个脚本告诉Redis:只有当它存在并且存储在键上的值正是期望的值时才删除键.
 *  luar脚本:
 * if redis.call("get",KEYS[1]) == ARGV[1] then
 *   return redis.call("del",KEYS[1])
 * else
 *   return 0
 * end
 *
 * 分布式场景正确的实现方式:
 * 在分布式场景中,假设我们有N个Redis主机(即N个Master).这些节点是完全独立的,所以我们不使用复制或任何其他隐式协调系统.
 *   1,它以毫秒为单位获取当前时间
 *   2,它尝试按顺序在所有N个实例中获取锁,在所有实例中使用相同的键名和随机值.在步骤2中,当在每个实例中设置锁时,客户机使用一个超时,
 * 这个超时与总锁自动释放时间相比很小,以便获得锁.例如,如果自动释放时间为10秒,则超时时间可以在5-50毫秒范围内.这可以防止客户端长时间阻塞,
 * 试图与一个关闭的Redis节点进行通信:如果一个实例不可用,我们应该尝试尽快与下一个实例进行通信.
 *   3,客户机通过从当前时间减去步骤1中获得的时间戳来计算获得锁所花费的时间.当且仅当客户端能够在大多数实例(至少3个)中获取锁,并且获取锁
 * 所花费的总时间小于锁的有效时间,则认为该锁已被获取.
 *   4,如果获得了锁,则认为其有效时间等于初始有效时间减去经过的时间,如步骤3中计算的那样.
 *   5,如果客户端由于某种原因(无法锁定N/2+1个实例,或者有效时间为负数)未能获得锁,它将尝试解锁所有实例(甚至是它认为无法锁定的实例).
 *     此算法的前提:redis节点之间的时钟一致,且以几乎一致的频率更新.
 *     互斥规则:只有当持有锁的客户端在锁有效时间内(如步骤3中获得的)减去一些时间(为了补偿进程之间的时钟漂移,只需几毫秒)终止其工作时,
 * 才能保证互斥规则.
 *
 * 安全性:
 *     首先,让我们假设客户端能够在大多数实例中获取锁.所有实例都将包含一个具有相同存活时间的键.但是,密钥是在不同的时间设置的,
 * 因此密钥也将在不同的时间过期.但是,如果第一个密钥在最坏的情况下在时间T1(我们在联系第一个服务器之前进行采样的时间)时被设置,
 * 最后一个密钥也在最坏的情况下在时间T2(我们从最后一个服务器获得应答的时间)时被设置,那么我们可以肯定,在集合中过期的第一个密钥将至少
 * 存在MIN_VALIDITY=TTL-(T2-T1)- clock_drift.所有其他密钥将在稍后过期,因此我们确保至少在此时间内将同时设置密钥.
 *     如果客户端锁定大多数实例的时间接近或大于锁的最大有效时间(我们基本上使用SET的TTL),它将认为锁无效并将解锁实例.因此我们只需要
 * 考虑客户端能够在小于有效时间的时间内锁定大多数实例的情况.在这种情况下,对于上面已经表达的参数,对于MIN_VALIDITY,任何客户端都不应该
 * 能够重新获取锁.因此,只有当锁定大多数实例的时间大于TTL时间时,多个客户端才能同时锁定N/2+1个实例(“time”是步骤2的末尾),从而使锁定无效
 *
 * 在设置大多数密钥期间，其他客户端将无法获得锁，因为如果已经存在N/2+1个密钥，则N/2+1个set NX操作将无法成功。因此，如果获得了锁，则不可能同时重新获得它(违反互斥属性)。
 *
 *
 *
 * 但是，我们还希望确保同时尝试获取锁的多个客户端不能同时成功。
 * 如果客户端锁定大多数实例的时间接近或大于锁的最大有效时间(我们基本上使用SET的TTL)，它将认为锁无效并将解锁实例，因此我们只需要考虑客户端能够在小于有效时间的时间内锁定大多数实例的情况。在这种情况下，对于上面已经表达的参数，对于MIN_VALIDITY，任何客户端都不应该能够重新获取锁。因此，只有当锁定大多数实例的时间大于TTL时间时，多个客户端才能同时锁定N/2+1个实例(“time”是步骤2的末尾)，从而使锁定无效
 */
@Slf4j
public class RedLock {
    public static void main(String[] args) {
    }

    public static void test() throws Exception {
        RedissonClient c = client();
        RFencedLock lock = c.getFencedLock("myFenced");

        log.info("shutdown");
        c.shutdown();
    }

    public static RedissonClient client(){
        Config cfg = new Config();
        cfg.useSingleServer().setAddress("redis://192.168.0.125:6379");
        RedissonClient client = Redisson.create(cfg);
        return client;
    }
}
